package org.peterf.foxish.engine;

import java.util.Stack;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import org.peterf.foxish.camera.Camera;
import org.peterf.foxish.engine.input.FingerEvent;
import org.peterf.foxish.engine.input.InputEngine;
import org.peterf.foxish.engine.input.OnTouchListener;
import org.peterf.foxish.engine.setting.ScreenSetting;
import org.peterf.foxish.opengl20.OpenGLInfo;
import org.peterf.foxish.opengl20.OpenGLUtil;
import org.peterf.foxish.program.ProgramRepository;
import org.peterf.foxish.scene.Scene;
import org.peterf.foxish.scene.Scene.SceneState;
import org.peterf.foxish.surface.SurfaceView;
import org.peterf.foxish.texture.TextureRepository;
import org.peterf.foxish.util.ActivityUtil;
import org.peterf.foxish.util.ResourceUtil;

import android.app.Activity;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.os.Handler;
import android.os.Message;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;

/**
 * The engine contains some sub-engines.
 * <p>
 * The engine maintains a stack of {@link Scene}s for rendering. The top
 * {@link Scene} of the stack is responsible for rendering. You can make a
 * {@link Scene} the current one by calling {@link #pushScene(Scene)}, in which
 * case the previous one would stay in the stack until {@link #popScene()} is
 * called.
 * <p>
 * Note that {@link Scene#onCreate()} will be called only when the surface is
 * valid.
 * 
 * @author peter
 */
public class Engine implements Renderer, OnTouchListener, OnKeyListener {

    public enum EngineState {
        DRAW, DRAW_PAUSE, INIT, PAUSED
    }

    private static Engine instance = null;
    
    private static final int MSG_SHORT_TOAST = 0x1;
    
    private static final int MSG_LONG_TOAST = 0x2;
    
    private Handler handler = new Handler() {
        
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_SHORT_TOAST: {
                ActivityUtil.shortToast((Activity) context, msg.arg1);
                break;
            }
            case MSG_LONG_TOAST: {
                ActivityUtil.longToast((Activity) context, msg.arg1);
                break;
            }
            }
        }
        
    };

    public void msgShortToast(int id) {
        Message msg = Message.obtain();
        msg.what = MSG_SHORT_TOAST;
        msg.arg1 = id;
        handler.sendMessage(msg);
    }
    
    public void msgLongToast(int id) {
        Message msg = Message.obtain();
        msg.what = MSG_LONG_TOAST;
        msg.arg1 = id;
        handler.sendMessage(msg);
    }
    
    /**
     * Get the global engine.
     * 
     * @param context
     *            System context.
     * @param screenSetting
     *            Screen setting for the engine.
     * @return The engine instance which is created upon the first call.
     */
    public static Engine createInstance(Context context,
            ScreenSetting screenSetting) {
        if (instance == null) {
            instance = new Engine(context, screenSetting);
        }
        return instance;
    }

    /**
     * Destroy the global instance and release all resources.
     */
    public static void destroyInstance() {
        if (instance != null) {
            instance = null;
            ProgramRepository.destroy();
            TextureRepository.destroy();
            // TODO Anything else?
            // TODO Could this be called before OpenGL context destruction?
        }
    }

    /**
     * 
     * @return The global engine instance.
     */
    public static Engine getInstance() {
        return instance;
    }

    private Camera camera = null;

    private Context context;

    private InputEngine inputEngine;

    private boolean isSurfaceCreated = false;

    private long lastDraw = 0L;

    private long lastPause = 0L;

    private Stack<Scene> sceneStack = new Stack<Scene>();
    
    private Stack<Scene> garbageStack = new Stack<Scene>();

    private ScreenSetting screenSetting;

    private EngineState state = EngineState.INIT;

    private SurfaceView surface = null;

    /**
     * Initialize the engine with a given screen setting.
     * 
     * @param context
     *            System context.
     * @param screenSetting
     *            The screen setting.
     */
    private Engine(Context context, ScreenSetting screenSetting) {
        this.context = context;
        this.screenSetting = screenSetting;
        OpenGLInfo.setContext(context);
        ResourceUtil.setContext(context);
        createSurface();
        createInputEngine();
    }

    public Camera getCamera() {
        return camera;
    }

    /**
     * 
     * @return System context.
     */
    public Context getContext() {
        return context;
    }

    public InputEngine getInputEngine() {
        return inputEngine;
    }

    /**
     * 
     * @return The screen setting.
     */
    public ScreenSetting getScreenSetting() {
        return screenSetting;
    }

    public EngineState getState() {
        return state;
    }

    public SurfaceView getSurface() {
        return surface;
    }

    public void onDrawFrame(GL10 gl) {
        if (lastDraw == 0L) {
            lastDraw = System.nanoTime();
        }
        float elapsed = (float) (System.nanoTime() - lastDraw) / 1000000000L;
        lastDraw = System.nanoTime();
        // TODO Place the following to the right place.
        GLES20.glEnable(GLES20.GL_BLEND);
        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
        if (camera != null) {
            synchronized (sceneStack) {
                if (!sceneStack.empty()) {
                    Scene top;
                    do {
                        top = sceneStack.peek();
                        if (top.getState() == SceneState.INIT) {
                            top.onCreate();
                        } else if (top.getState() == SceneState.INVALID) {
                            top.onRecreate();
                        }
                    } while (top != sceneStack.peek());
                    top.setState(SceneState.VISIBLE);
                    // TODO Place the following to the right place.
                    OpenGLUtil.clearColor(1, 1, 1, 1);
                    top.onDraw(camera, elapsed);
                }
            }
        }
        synchronized (garbageStack) {
            while (!garbageStack.empty()) {
                garbageStack.pop().onDestroy();
            }
        }
    }

    public void onPause() {
        surface.onPause();
        lastPause = System.nanoTime();
        state = EngineState.PAUSED;
    }

    public void onResume() {
        if (lastDraw == 0L) {
            lastDraw = System.nanoTime();
        } else {
            if (lastPause != 0L) {
                lastDraw += (System.nanoTime() - lastPause);
                lastPause = 0L;
            }
        }
        // TODO Draw pause scene.
        surface.onResume();
    }

    public void onSurfaceChanged(GL10 gl, int width, int height) {
        OpenGLUtil.setViewPort(width, height);
    }

    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        state = EngineState.DRAW;
        synchronized (sceneStack) {
            if (isSurfaceCreated) {
                ProgramRepository.recreate();
                TextureRepository.recreate();
                for (Scene e : sceneStack) {
                    if (e.getState() != SceneState.INIT) {
                        e.setState(SceneState.INVALID);
                    }
                }
                if (!sceneStack.empty()) {
                    Scene top = sceneStack.peek();
                    top.onRecreate();
                    top.setState(SceneState.VISIBLE);
                }
            } else {
                if (!sceneStack.empty()) {
                    Scene top = sceneStack.peek();
                    if (top.getState() == SceneState.INIT) {
                        top.onCreate();
                        top.setState(SceneState.VISIBLE);
                    }
                }
                isSurfaceCreated = true;
            }
        }
    }

    public boolean onTouch(FingerEvent event) {
        Scene current = null;
        synchronized (sceneStack) {
            if (!sceneStack.empty()) {
                current = sceneStack.peek();
            }
        }
        if (current != null) {
            return current.onTouch(event);
        } else {
            return false;
        }
    }

    public void popScene() {
        synchronized (sceneStack) {
            synchronized (garbageStack) {
                if (!sceneStack.empty()) {
                    garbageStack.push(sceneStack.pop());
                }
            }
        }
    }

    public void pushScene(Scene newScene) {
        synchronized (sceneStack) {
            if (state == EngineState.DRAW) {
                if (!sceneStack.empty()) {
                    sceneStack.peek().onStored();
                    sceneStack.peek().setState(SceneState.CREATED);
                }
            }
            sceneStack.push(newScene);
        }
    }

    public void replaceScene(Scene newScene) {
        synchronized (sceneStack) {
            popScene();
            pushScene(newScene);
        }
    }

    public void setCamera(Camera camera) {
        this.camera = camera;
    }

    private void createInputEngine() {
        inputEngine = new InputEngine(this);
        inputEngine.attachSurface(surface);
    }

    private void createSurface() {
        if (surface == null) {
            surface = new SurfaceView(this);
            surface.setRenderer(this);
            switch (screenSetting.getUpdateStrategy()) {
            case UPDATE_CONTINUOUSLY: {
                surface.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
                break;
            }
            case UPDATE_WHEN_DIRTY: {
                // TODO Dirty draw.
                surface.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
                break;
            }
            }
        }
    }

    public boolean onKey(View v, int keyCode, KeyEvent event) {
        Scene current = null;
        synchronized (sceneStack) {
            if (!sceneStack.empty()) {
                current = sceneStack.peek();
            }
        }
        if (current != null) {
            return current.onKey(v, keyCode, event);
        } else {
            return false;
        }
    }

}
